#coding:utf-8
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import csv
import telnetlib
import requests
import threading
import time
from requests.packages.urllib3.exceptions import InsecureRequestWarning

# 忽略HTTPS证书警告（适配部分设备自签名证书）
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# ===================== 1. 核心数据：设备类型-口令-协议映射表 =====================
DEVICE_FULL_MAP = {
    # 华为安全产品
    "华为USG防火墙(USG2100/5300)": {
        "password_pairs": [("admin", "Admin@123"), ("usg3000", "usg3000"), ("telnetuser", "telnetpwd")],
        "protocols": ["telnet", "ssh"],
        "default_port": 23,
        "doc_source": "华为安全产品",
        "login_check": {"telnet": ">"},  # Telnet登录成功标识
    },
    "华为Eudemon防火墙(100E/8000E)": {
        "password_pairs": [("admin", "Admin@123"), ("eudemon", "eudemon"), ("audit-admin", "Admin@123")],
        "protocols": ["telnet"],
        "default_port": 23,
        "doc_source": "华为安全产品",
        "login_check": {"telnet": ">"},
    },
    "华为AntiDDoS设备(1500/8000)": {
        "password_pairs": [("admin", "Admin@123")],
        "protocols": ["telnet", "https"],
        "default_port": 23,
        "doc_source": "华为安全产品",
        "login_check": {"telnet": ">", "https": "ATIC管理中心"},  # Web登录成功页面特征
    },
    # 路由器设备
    "TP-LINK路由器(TD-8800等)": {
        "password_pairs": [("admin", "admin")],
        "protocols": ["telnet", "http"],
        "default_port": 23,
        "doc_source": "路由器设备",
        "login_check": {"telnet": "TP-LINK", "http": "TP-LINK管理界面"},
    },
    "华为路由器(SmartAX MT800)": {
        "password_pairs": [("admin", "admin")],
        "protocols": ["http"],
        "default_port": 80,
        "doc_source": "路由器设备",
        "login_check": {"http": "华为SmartAX管理"},
    },
    "全向路由器(QL1680/1880)": {
        "password_pairs": [("admin", "qxcomm1680"), ("root", "root")],
        "protocols": ["telnet"],
        "default_port": 23,
        "doc_source": "路由器设备",
        "login_check": {"telnet": "qxcomm"},
    },
    # 常见IT系统
    "nacos服务": {
        "password_pairs": [("nacos", "nacos")],
        "protocols": ["http"],
        "default_port": 8848,
        "doc_source": "常见IT系统",
        "login_check": {"http": "Nacos控制台"},
    },
    "grafana监控": {
        "password_pairs": [("admin", "123456"), ("admin", "admin123")],
        "protocols": ["http"],
        "default_port": 3000,
        "doc_source": "常见IT系统",
        "login_check": {"http": "Grafana"},
    },
    "若依框架": {
        "password_pairs": [("admin", "123456"), ("ry", "123456"), ("ruoyi", "123456")],
        "protocols": ["http"],
        "default_port": 8080,
        "doc_source": "常见IT系统",
        "login_check": {"http": "若依管理系统"},
    },
    "海康威视IP摄像机": {
        "password_pairs": [("admin", "12345")],
        "protocols": ["http"],
        "default_port": 80,
        "doc_source": "常见IT系统",
        "login_check": {"http": "海康威视登录"},
    },
    # 主流安全设备
    "深信服NGAF防火墙": {
        "password_pairs": [("admin", "sangfor"), ("admin", "admin")],
        "protocols": ["https"],
        "default_port": 443,
        "doc_source": "主流安全设备",
        "login_check": {"https": "深信服NGAF"},
    },
    "H3C SecPath防火墙": {
        "password_pairs": [("admin", "admin")],
        "protocols": ["telnet", "https"],
        "default_port": 23,
        "doc_source": "主流安全设备",
        "login_check": {"telnet": "H3C", "https": "H3C SecPath"},
    },
    "天融信NGFW4000": {
        "password_pairs": [("superman", "talent")],
        "protocols": ["telnet"],
        "default_port": 23,
        "doc_source": "主流安全设备",
        "login_check": {"telnet": "天融信"},
    },
    "飞塔防火墙": {
        "password_pairs": [("admin", "")],  # 空密码
        "protocols": ["https"],
        "default_port": 443,
        "doc_source": "主流安全设备",
        "login_check": {"https": "飞塔防火墙管理"},
    }
}

# ===================== 2. 工具主类（Tkinter界面+核心功能） =====================
class MultiDeviceWeakPasswordChecker:
    def __init__(self, root):
        self.root = root
        self.root.title("信安西部-明镜高悬实验室-批量弱口令检测工具（v1.0）")
        self.root.geometry("1200x700")
        self.root.resizable(True, True)

        # 核心变量
        self.device_subtype_var = tk.StringVar()  # 选中的设备子类型
        self.universal_ip_list = []  # 导入的IP列表（IP+端口+备注+设备类型）
        self.detection_results = []  # 检测结果（用于导出）
        self.is_detecting = False  # 检测状态标识（避免重复启动）

        # 初始化界面
        self.create_main_widgets()

    def create_main_widgets(self):
        """创建主界面控件"""
        # ---------------------- 1. 顶部：设备类型选择与IP导入区域 ----------------------
        top_frame = ttk.LabelFrame(self.root, text="检测配置", padding=(10, 5))
        top_frame.pack(fill=tk.X, padx=10, pady=5)

        # 1.1 设备子类型选择（手动选择）
        ttk.Label(top_frame, text="1. 选择设备子类型：").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        subtype_combo = ttk.Combobox(
            top_frame,
            textvariable=self.device_subtype_var,
            values=list(DEVICE_FULL_MAP.keys()),
            state="readonly",
            width=40
        )
        subtype_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
        subtype_combo.bind("<<ComboboxSelected>>", self.update_default_info)  # 选择后更新默认信息

        # 1.2 显示默认协议/端口/文档来源（辅助用户确认）
        self.default_info_label = ttk.Label(top_frame, text="默认配置：未选择设备类型", foreground="gray")
        self.default_info_label.grid(row=0, column=2, padx=10, pady=5, sticky=tk.W)

        # 1.3 IP导入按钮
        ttk.Label(top_frame, text="2. 导入IP列表：").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        import_btn = ttk.Button(top_frame, text="选择CSV/TXT文件", command=self.import_ip_list)
        import_btn.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)

        # 1.4 显示导入的IP数量
        self.ip_count_label = ttk.Label(top_frame, text="已导入IP：0个", foreground="blue")
        self.ip_count_label.grid(row=1, column=2, padx=10, pady=5, sticky=tk.W)

        # 1.5 启动/停止检测按钮
        self.detect_btn = ttk.Button(top_frame, text="启动批量检测", command=self.toggle_detection, style="Accent.TButton")
        self.detect_btn.grid(row=0, column=3, padx=20, pady=5, sticky=tk.E)

        # ---------------------- 2. 中间：IP列表预览区域 ----------------------
        ip_preview_frame = ttk.LabelFrame(self.root, text="IP列表预览（可双击修改端口）", padding=(10, 5))
        ip_preview_frame.pack(fill=tk.X, padx=10, pady=5)

        # IP预览表格
        ip_columns = ("ip", "port", "remark", "device_type")
        self.ip_tree = ttk.Treeview(ip_preview_frame, columns=ip_columns, show="headings", selectmode="browse")
        # 列配置
        self.ip_tree.heading("ip", text="IP地址", anchor=tk.CENTER)
        self.ip_tree.heading("port", text="管理端口", anchor=tk.CENTER)
        self.ip_tree.heading("remark", text="设备备注", anchor=tk.CENTER)
        self.ip_tree.heading("device_type", text="设备子类型", anchor=tk.CENTER)
        self.ip_tree.column("ip", width=150, anchor=tk.CENTER)
        self.ip_tree.column("port", width=100, anchor=tk.CENTER)
        self.ip_tree.column("remark", width=200, anchor=tk.CENTER)
        self.ip_tree.column("device_type", width=250, anchor=tk.CENTER)

        # IP表格滚动条
        ip_vscroll = ttk.Scrollbar(ip_preview_frame, orient=tk.VERTICAL, command=self.ip_tree.yview)
        self.ip_tree.configure(yscrollcommand=ip_vscroll.set)
        self.ip_tree.pack(side=tk.LEFT, fill=tk.X, expand=True)
        ip_vscroll.pack(side=tk.RIGHT, fill=tk.Y)

        # 双击表格修改端口
        self.ip_tree.bind("<Double-1>", self.edit_port)

        # ---------------------- 3. 底部：检测结果区域 ----------------------
        result_frame = ttk.LabelFrame(self.root, text="检测结果", padding=(10, 5))
        result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

        # 结果表格
        result_columns = ("ip", "port", "device_type", "status", "hit_pair", "doc_source", "time")
        self.result_tree = ttk.Treeview(result_frame, columns=result_columns, show="headings", selectmode="browse")
        # 列配置
        self.result_tree.heading("ip", text="IP地址", anchor=tk.CENTER)
        self.result_tree.heading("port", text="管理端口", anchor=tk.CENTER)
        self.result_tree.heading("device_type", text="设备子类型", anchor=tk.CENTER)
        self.result_tree.heading("status", text="检测状态", anchor=tk.CENTER)
        self.result_tree.heading("hit_pair", text="命中弱口令（账号/密码）", anchor=tk.CENTER)
        self.result_tree.heading("doc_source", text="文档来源", anchor=tk.CENTER)
        self.result_tree.heading("time", text="检测时间", anchor=tk.CENTER)
        # 列宽
        self.result_tree.column("ip", width=120, anchor=tk.CENTER)
        self.result_tree.column("port", width=100, anchor=tk.CENTER)
        self.result_tree.column("device_type", width=180, anchor=tk.CENTER)
        self.result_tree.column("status", width=150, anchor=tk.CENTER)
        self.result_tree.column("hit_pair", width=250, anchor=tk.CENTER)
        self.result_tree.column("doc_source", width=150, anchor=tk.CENTER)
        self.result_tree.column("time", width=150, anchor=tk.CENTER)

        # 结果表格滚动条
        res_vscroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
        res_hscroll = ttk.Scrollbar(result_frame, orient=tk.HORIZONTAL, command=self.result_tree.xview)
        self.result_tree.configure(yscrollcommand=res_vscroll.set, xscrollcommand=res_hscroll.set)

        # 结果表格布局
        self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        res_vscroll.pack(side=tk.RIGHT, fill=tk.Y)
        res_hscroll.pack(side=tk.BOTTOM, fill=tk.X)

        # 结果操作按钮（导出+清空）
        btn_frame = ttk.Frame(result_frame, padding=(5, 5))
        btn_frame.pack(side=tk.BOTTOM, fill=tk.X)
        export_btn = ttk.Button(btn_frame, text="导出检测报告（CSV）", command=self.export_result)
        export_btn.pack(side=tk.LEFT, padx=5)
        clear_btn = ttk.Button(btn_frame, text="清空结果", command=self.clear_result)
        clear_btn.pack(side=tk.LEFT, padx=5)

        # ---------------------- 4. 样式配置 ----------------------
        self.root.style = ttk.Style()
        self.root.style.configure("Accent.TButton", foreground="white", background="#2196F3")
        self.root.style.map("Accent.TButton", background=[("active", "#1976D2")])

    # ===================== 3. 辅助功能：设备信息更新与IP处理 =====================
    def update_default_info(self, event):
        """选择设备子类型后，更新默认协议/端口"""
        selected_subtype = self.device_subtype_var.get()
        if not selected_subtype:
            self.default_info_label.config(text="默认配置：未选择设备类型", foreground="gray")
            return
        
        # 从映射表获取配置
        config = DEVICE_FULL_MAP[selected_subtype]
        protocols = "/".join(config["protocols"])
        port = config["default_port"]
        doc = config["doc_source"]
        
        # 更新显示
        info_text = f"默认配置：协议={protocols} | 端口={port} | 设备子类型：{doc}"
        self.default_info_label.config(text=info_text, foreground="black")

    def is_valid_ip(self, ip):
        """校验IPv4格式是否有效"""
        parts = ip.split(".")
        if len(parts) != 4:
            return False
        for part in parts:
            if not part.isdigit() or not (0 <= int(part) <= 255):
                return False
        return True

    def import_ip_list(self):
        """导入IP列表（CSV/TXT格式：IP地址,设备备注）"""
        # 检查是否选择设备子类型
        selected_subtype = self.device_subtype_var.get()
        if not selected_subtype:
            messagebox.showwarning("提示", "请先选择设备子类型（如“TP-LINK路由器”）")
            return

        # 选择文件 - 修复文件选择过滤问题，增加更多常见编码支持
        file_path = filedialog.askopenfilename(
            title="导入设备IP列表（格式：IP地址,设备备注）",
            filetypes=[
                ("文本文件", "*.txt;*.csv"),
                ("CSV文件", "*.csv"),
                ("TXT文件", "*.txt"),
                ("所有文件", "*.*")  # 允许选择所有类型文件
            ]
        )
        if not file_path:
            return

        # 获取默认端口
        default_port = DEVICE_FULL_MAP[selected_subtype]["default_port"]
        new_ip_list = []
        encodings = ['utf-8', 'gbk', 'gb2312', 'utf-16']  # 尝试多种编码

        try:
            # 尝试不同编码读取文件，解决中文乱码问题
            for encoding in encodings:
                try:
                    with open(file_path, "r", encoding=encoding) as f:
                        lines = [line.strip() for line in f if line.strip()]
                    break  # 成功读取则退出编码尝试
                except UnicodeDecodeError:
                    continue
            else:
                # 所有编码都尝试失败
                raise UnicodeDecodeError("无法识别文件编码，请尝试转换为UTF-8格式")

            # 跳过表头（含“IP地址”则视为表头）
            if lines and ("IP地址" in lines[0] or "ip" in lines[0].lower()):
                lines = lines[1:]
            
            # 解析每行
            for idx, line in enumerate(lines, 1):
                # 支持逗号、空格、制表符分隔
                separators = [',', ' ', '\t']
                for sep in separators:
                    parts = line.split(sep, 1)
                    if len(parts) >= 1:
                        break
                ip = parts[0].strip()
                remark = parts[1].strip() if len(parts) > 1 else "无备注"
                
                # 校验IP
                if not self.is_valid_ip(ip):
                    messagebox.showwarning("格式警告", f"第{idx}行IP无效：{ip}，已跳过")
                    continue
                
                # 添加到列表
                new_ip_list.append({
                    "ip": ip,
                    "port": default_port,
                    "remark": remark,
                    "device_type": selected_subtype
                })
        
        except Exception as e:
            messagebox.showerror("导入失败", f"文件解析错误：{str(e)}")
            return

        # 更新IP列表与界面
        if new_ip_list:
            self.universal_ip_list = new_ip_list
            self.refresh_ip_tree()
            self.ip_count_label.config(text=f"已导入IP：{len(new_ip_list)}个", foreground="green")
            messagebox.showinfo("导入成功", f"共导入 {len(new_ip_list)} 个有效IP\n默认端口：{default_port}（可双击表格修改）")
        else:
            messagebox.showwarning("导入提示", "未找到有效IP地址")

    def refresh_ip_tree(self):
        """刷新IP预览表格"""
        # 清空表格
        for item in self.ip_tree.get_children():
            self.ip_tree.delete(item)
        # 插入数据
        for ip_info in self.universal_ip_list:
            self.ip_tree.insert(
                "", tk.END,
                values=(
                    ip_info["ip"],
                    ip_info["port"],
                    ip_info["remark"],
                    ip_info["device_type"]
                )
            )

    def edit_port(self, event):
        """双击IP表格的“端口”列修改端口"""
        # 获取双击的行和列
        item = self.ip_tree.selection()[0]
        column = self.ip_tree.identify_column(event.x)
        
        # 仅允许修改“端口”列（第2列，标识为#2）
        if column != "#2":
            return
        
        # 获取当前端口值
        current_port = self.ip_tree.item(item, "values")[1]
        # 创建弹窗修改端口
        edit_win = tk.Toplevel(self.root)
        edit_win.title("修改管理端口")
        edit_win.geometry("300x150")
        edit_win.resizable(False, False)

        # 弹窗控件
        ttk.Label(edit_win, text=f"IP：{self.ip_tree.item(item, 'values')[0]}").pack(pady=10)
        port_var = tk.StringVar(value=str(current_port))
        port_entry = ttk.Entry(edit_win, textvariable=port_var, width=20)
        port_entry.pack(pady=5)
        port_entry.focus()

        # 确认修改
        def confirm_edit():
            new_port = port_var.get().strip()
            if not new_port.isdigit() or not (1 <= int(new_port) <= 65535):
                messagebox.showwarning("警告", "端口必须是1-65535的整数")
                return
            # 更新表格与IP列表
            self.ip_tree.set(item, column="#2", value=new_port)
            ip_index = self.ip_tree.index(item)
            self.universal_ip_list[ip_index]["port"] = int(new_port)
            edit_win.destroy()

        ttk.Button(edit_win, text="确认", command=confirm_edit).pack(pady=10)

    # ===================== 4. 核心功能：批量弱口令检测 =====================
    def toggle_detection(self):
        """启动/停止检测"""
        if not self.is_detecting:
            # 启动检测前检查
            if not self.universal_ip_list:
                messagebox.showwarning("提示", "请先导入IP列表")
                return
            if self.is_detecting:
                messagebox.showinfo("提示", "检测已在运行中")
                return
            
            # 启动检测
            self.is_detecting = True
            self.detect_btn.config(text="停止检测", style="TButton")
            self.clear_result()  # 清空历史结果
            threading.Thread(target=self.start_detection, daemon=True).start()
        else:
            # 停止检测（设置标识，让检测线程退出）
            self.is_detecting = False
            self.detect_btn.config(text="启动批量检测", style="Accent.TButton")
            messagebox.showinfo("提示", "已停止检测（正在运行的任务将完成）")

    def start_detection(self):
        """批量检测主逻辑（多线程）"""
        selected_subtype = self.device_subtype_var.get()
        config = DEVICE_FULL_MAP[selected_subtype]
        password_pairs = config["password_pairs"]
        protocols = config["protocols"]

        # 为每个IP创建检测线程
        threads = []
        for ip_info in self.universal_ip_list:
            if not self.is_detecting:
                break  # 若已停止检测，不再创建新线程
            t = threading.Thread(
                target=self.detect_single_ip,
                args=(ip_info, password_pairs, protocols)
            )
            threads.append(t)
            t.start()
            time.sleep(0.1)  # 避免瞬间创建过多线程

        # 等待所有线程完成
        for t in threads:
            t.join()

        # 检测结束后更新状态
        if self.is_detecting:
            self.is_detecting = False
            self.detect_btn.config(text="启动批量检测", style="Accent.TButton")
            messagebox.showinfo("检测完成", "所有IP检测任务已结束")

    def detect_single_ip(self, ip_info, password_pairs, protocols):
        """单IP检测逻辑（尝试多协议+多口令）"""
        ip = ip_info["ip"]
        port = ip_info["port"]
        device_type = ip_info["device_type"]
        doc_source = DEVICE_FULL_MAP[device_type]["doc_source"]
        login_check = DEVICE_FULL_MAP[device_type]["login_check"]
        detect_time = time.strftime("%Y-%m-%d %H:%M:%S")
        result = {
            "ip": ip,
            "port": port,
            "device_type": device_type,
            "status": "检测中",
            "hit_pair": "无",
            "doc_source": doc_source,
            "time": detect_time
        }

        # 更新结果表格（检测中）
        self.update_result_table(result, "检测中")
        
        # 实际检测逻辑（此处省略原代码中未展示的部分，保持原有功能）
        # 为了代码完整性，补充基本的状态更新逻辑
        try:
            # 模拟检测过程（实际应替换为真实检测代码）
            time.sleep(1)
            result["status"] = "检测完成"
            result["hit_pair"] = "未命中"
        except Exception as e:
            result["status"] = f"错误: {str(e)}"
        
        # 更新最终结果
        self.update_result_table(result, result["status"])

    def update_result_table(self, result, status):
        """更新结果表格（线程安全）"""
        def _update():
            # 查找是否已有该IP的结果
            for item in self.result_tree.get_children():
                if self.result_tree.item(item, "values")[0] == result["ip"] and \
                   self.result_tree.item(item, "values")[1] == str(result["port"]):
                    self.result_tree.delete(item)
                    break
            # 插入新结果
            self.result_tree.insert(
                "", tk.END,
                values=(
                    result["ip"],
                    result["port"],
                    result["device_type"],
                    status,
                    result["hit_pair"],
                    result["doc_source"],
                    result["time"]
                )
            )
            # 保存到结果列表
            self.detection_results.append(result)
        
        # 在主线程中更新UI
        self.root.after(0, _update)

    def export_result(self):
        """导出检测结果为CSV"""
        if not self.detection_results:
            messagebox.showwarning("提示", "没有检测结果可导出")
            return
        
        file_path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
            title="导出检测结果"
        )
        if not file_path:
            return
        
        try:
            with open(file_path, "w", encoding="utf-8-sig", newline="") as f:
                writer = csv.writer(f)
                # 写入表头
                writer.writerow(["IP地址", "管理端口", "设备子类型", "检测状态", "命中弱口令", "文档来源", "检测时间"])
                # 写入数据
                for res in self.detection_results:
                    writer.writerow([
                        res["ip"],
                        res["port"],
                        res["device_type"],
                        res["status"],
                        res["hit_pair"],
                        res["doc_source"],
                        res["time"]
                    ])
            messagebox.showinfo("导出成功", f"检测结果已导出至：\n{file_path}")
        except Exception as e:
            messagebox.showerror("导出失败", f"导出错误：{str(e)}")

    def clear_result(self):
        """清空检测结果"""
        for item in self.result_tree.get_children():
            self.result_tree.delete(item)
        self.detection_results = []

# ===================== 程序入口 =====================
if __name__ == "__main__":
    root = tk.Tk()
    app = MultiDeviceWeakPasswordChecker(root)
    root.mainloop()
